<?php
/**
* active record class, forms the back bone of all database related classes
*
 * The ActiveRecord class allows for a single database record to be
 * manipulated (Created, Read, Updated, and Deleted) without any
 * item-specific SQL (Don't Repeat Yourself).
*@author		mike tomasello <miketomasello@gmail.com>
*@license		bsd
*@version		0.5
*@package		core
 */
class ActiveRecord {

	/**
	 * The $table property indicates which database table the class corresponds to (that is, to which table does
	 * this record belong).
	 *
	 * @var string
	 */
	protected $table = '';
	/**
	 * The $id property represents the record's primary key and is used to retrieve its relevant data.
	 *
	 * @var int
	 */
	public $id = 0;
	/**
	 * The $data property is an array containing a record's data in the form it is stored in the database. This
	 * property is automatically populated when a programmer attempts to access record data.
	 *
	 * Transformed record data (e.g. an author ID into a User object) should not be stored in this array but
	 * rather in its own property that is converted back and forth using magic methods.
	 *
	 * @var mixed
	 */
	public $data = array();
	/**
	 * $transformed serves a similar purpose to the $data property except that it contains data in a transformed
	 * state. When a controller or other external code snippet accesses data in an ActiveRecord class, if there
	 * is relevant data in the $transformed property it will be used instead of the $data property.
	 *
	 * @var mixed
	 */
	public $transformed = array();
	/**
	 * $hasData is 'false' until data is populated or saved, whence it becomes 'true'.
	 *
	 * @var bool
	 */
	public $hasData = false;

	/**
	 * The $cache property is used to cache the data of objects that have already been called.
	 *
	 * @var ActiveRecord
	 */
	static $cache = array();

	# In the majority of cases this will be overridden, nevertheless it is an operation that should always
	# take place and is therefore logical to have in our base constructor.
	function __construct ($id = 0)
	{
		$this->id = $id;
	}

	/**
	 * The populate method retrieves data related to this item from its table in the database. The method
	 * has no arguments, though requires the $id property to be set to function correctly.
	 *
	 * This method generally need not be called by the model programmer, as it is automatically called when
	 * an object's data properties are accessed for the first time.
	 *
	 * To hook into the execution of this method — e.g. for transformation of a numeric ID to an object — you
	 * should define a __load method, which is automatically called after the succesful execution of this
	 * function.
	 *
	 * @return bool
	 */
	protected function populate ()
	{
		# Abort execution if the $table property is not set.
		if (empty($this->table))
		{
			throw new Exception('ActiveRecord has no table defined.');		
		}

		# An error should also be thrown if the id property is not set.
		if (empty($this->id))
		{
			throw new Exception('ActiveRecord has no ID set.');
		}

		if (!isset(ActiveRecord::$cache[get_class($this)]['data'][$this->id]))
		{
			# Currently we always use the 'id' field – in the future we will probably want to try and determine
			# the primary key if 'id' is not it.
			$query = new Query('SELECT * FROM `'.$this->table.'` WHERE `id` = ? LIMIT 0, 1', $this->id);
			if ($query->num_rows() > 0)
			{
				# We assign it to the static property $cache so that we don't have to redo this query					
				# each time this record is accessed.
				# Fetching by association retains a record's field names as property names.
				ActiveRecord::$cache[get_class($this)]['data'][$this->id] = $query->fetch_assoc();

				# Cache transformed properties as well as data.
				# This association means any changes made to the transformed properties will be echoed
				# in the cache.
				ActiveRecord::$cache[get_class($this)]['transformed'][$this->id] =& $this->transformed;
			}
			else
			{
				# If there is no data to retrieve consider it to be an Exception – something must be wrong data								# integrity wise for an object to be called with an invalid ID.
				throw new Exception('Item does not exist.');
			}
		}

		# Associate this record's data by reference to the $cache we put our data in above.
		$this->data =& ActiveRecord::$cache[get_class($this)]['data'][$this->id];
		# When we set this property to true it means this method won't be called again.
		$this->hasData = true;
		
		# Only transform data if transformed data is not cached.
		if (count($this->transformed) == 0)
		{
			# Programmers may use the '__load' pseudo-magic method if they wish to hook into the execution
			# of this function.
			if (method_exists($this, '__load')) $this->__load();
		}
		else
		{
			$this->transformed = ActiveRecord::$cache[get_class($this)]['transformed'][$this->id];
		}
	}

	/**
	 * The save method determines whether the record is new or existing. If the record is new, it inserts it
	 * into the relevant database table. If the record is existing, it updates the relevant record in the
	 * database table.
	 *
	 * Model programmers can hook into this method before it executes — e.g. for data transformations — by
	 * defining the pseudo-magic method '__save' in their classes. This method, if it exists, will be called
	 * first each time the 'save' method is called.
	 *
	 * @return bool
	 */
	final public function save ()
	{
		# A programmer might call the save method on an active record object that has had no changes,
		# in which case we just abort.
		if (!$this->hasData && $this->id != 0)
		{
			# The $hasData property is not set, but there is an ID, so this is an existing record
			# with no changes been made. Abort.
			return;
		}

		# Call the pseudo-magic method '__save' if is defined – this is for data transformations.
		if (method_exists($this, '__save')) $this->__save();

		# If the $hasData property is true then this record already exists and we just need to UPDATE it.
		# Otherwise we will have to INSERT it into the relevant table.
		if ($this->hasData)
		{
			# We need to build a string that is part of an SQL query for updating multiple fields in a record.
			# This query comes in the form of ... SET x=y, a=b, n=m WHERE ... We shall put these assignment
			# pairs into an array called $assignments and then convert it to a string.
			$assignments = array();
			# Traverse our data setting the fieldnames – which are the array's keys – to $fieldname and the data
			# they represent to $data.
			foreach ($this->data as $fieldname => $data)
			{
				# Escape any data.
				$data = Query::escape_string($data);

				# Append this assignment pair to our $assignments array.
				$assignments[] = "`{$fieldname}` = '{$data}'";
			}
			# Now we can convert our $assignments array into a comma delimited string.
			$assignments = implode(', ', $assignments);

			# Execute our completed query.
			$updateQuery = new Query('UPDATE `'.$this->table.'` SET
				'.$assignments.'
			WHERE `id` = \''.$this->id.'\'');

			return true;
		}
		# This record does not exist yet.
		else
		{
			# Inserting is much easier than updating as the Query class's insertion method maps well to our
			# record data array.
			$this->id = Query::insert($this->table, $this->data);
			# Now this object corresponds to a record in the table we should make sure it remembers it need not
			# refetch the data – do this by setting the $hasData property to true.
			$this->hasData = true;

			return true;
		}

		# Under normal circumstances code execution should not get to this point.
		return false;
	}

	/**
	 * The delete method does exactly what it says on the tin – deletes the active record from the database.
	 *
	 * When this method is called all of the ActiveRecord's properties will be reset.
	 */
	final public function delete ()
	{
		$deleteQuery = new Query('DELETE FROM `'.$this->table.'` WHERE `id` = ?', $this->id);

		# Reset the object's properties.
		$this->data = array();
		$this->hasData = false;
		$this->id = 0;
	}

	# We use the __get magic method (as well as __set) as a back and forth between object properties in PHP and
	# the record in the database. If a programmer tries to access a nonexistant property the request will be
	# sent through this method which will determine if it is a property of the record. If it is, then the relevant
	# data from the $data property — the property containing record data — will be returned.
	public function __get ($name)
	{
		# If we haven't retrieved the data from the database yet then we must do that before we can proceed.
		# The result of this is that the $data property is always populated on the first __get call.
		if ($this->hasData || (!$this->id && count($this->data)))
		{
			# If this data has had a data transformation performed upon it then we want to serve the transformed
			# data not the raw data.
			if (array_key_exists($name, $this->transformed))
			{
				# Arrays are a bit complicated. PHP5's __get and __set magic does not support arrays without
				# a bit of technowizardry: specifically we need to return arrays using the ArrayObject object:
				if (is_array($this->transformed[$name]))
				{
					# Return the array as an ArrayObject allowing the proper manipulation of this array through
					# overloading.
					return new ArrayObject($this->transformed[$name]);
				}
				else
				{
					# Since an array is not being accessed just return as normal.
					return $this->transformed[$name];
				}
			}
			
			# We have retrieved the record data, but the programmer still may be trying to access something that
			# doesn't exist. Determine if this is so.
			if (array_key_exists($name, $this->data))
			{
				# Everything is A-OK and we can return the requested data.
				return $this->data[$name];
			}
			else
			{
				# The programmer has tried to access nonexistant data so we throw an exception for her to work
				# with.
				throw new Exception('Field does not exist.');
			}
		}
		else 
		{
			# Since we have not retrieved the data yet we must do so; we do this by calling the populate method.
			$this->populate();
			# Rather than repeating the code above, we simply call this method again as $hasData should now be true.
			return $this->__get(&$name);
		}
	}

	# As the __get method, this method is used to forward requests for record data.
	final public function __set ($name, $value)
	{
		# We cannot save data non-destructively without all existing data already being assigned to this
		# object before any new data is set, so if the properties have not been populated and they can
		# be populated, we should do this now.
		if (!$this->hasData && $this->id !== 0)
		{
			# The $hasData property is not set, but there is an ID, so this is an existing record
			# not yet populated. Populate it.
			$this->populate();
		}

		# While the __set method doesn't need the record data to be populated in order to function, it does
		# require the correct field names. If the $hasData property is true then the field names are already
		# there. Similarly, if the $data property has any elements then the field names are also there.
		if ($this->hasData || count($this->data))
		{
			# We only allow data to be set for fields that actually exist.
			if (array_key_exists($name, $this->data))
			{
				# If the data is in transformed form (an object, or an array) we want to store it in the
				# transformed data array not the main one.
				if (is_object($value) || is_array($value))
				{
					$this->transformed[$name] = $value;
				}
				else
				{
					# This is raw data and should be stored in the main record data array.ß
					$this->data[$name] = $value;
				}
			}
			else 
			{
				# If the field does not exist, throw an exception.
				throw new Exception('Field does not exist.');
			}
		}
		else
		{
			# There's no data for us to fetch (we're assuming we're inserting data for a new record) but we still
			# need to know the array keys, so we fetch the field/column names from the table.
			$query = new Query('SHOW COLUMNS FROM `'.$this->table.'`');
			while(list($key) = $query->fetch_row())
				$this->data[$key] = null;

			# Now we have this data we can call this method again. We send the parameters by reference to avoid
			# increasing our memory footprint.
			$this->__set(&$name, &$value);
		}
	}

	# 'isset' calls on nonexistant properties are forwarded here. We simply forward the request on to the $data
	# property.
	final public function __isset ($name)
	{
		return isset($this->data[$name]);
	}

	/**
	 * The recordSet static function is used when we want to retrieve a set of data all at once. Retrieving each
	 * record seperately would result in a lot of superfluous queries being sent back and forth – this way we
	 * retrieve all the data we are going to use at once.
	 *
	 * The function returns an array containing objects of the type specified.
	 *
	 * @todo Determine type of return object via Late Static Binding instead of explicitly requesting it. This
	 * requires PHP 5.3+ to function (as LSB is not present before this).
	 * @return mixed
	 * @param Query $query The query to use to build the recordset.
	 * @param string $className That class of object that this recordset consists of.
	 */
	public static function recordSet (Query $query, $className = 'ActiveRecord')
	{
		# If there are no records in the set throw an exception.
		if ($query->num_rows() == 0)
		{
			throw new Exception('Cannot create a recordset with a query that returns no results.');
		}

		# Initialise the array we are going to return.
		$results = array();
		# We manipulate arrays as we go through them so we need to keep track of the index.
		$resultIndex = 0;

		# If the class specified has a __load method then we're going to have to call it.
		$callLoadMagic = method_exists($className, '__load');

		# As in the populate method, we fetch by association to keep track of field names as property names.
		while($data = $query->fetch_assoc())
		{
			# $className here represents the type of object. By default it is ActiveRecord.
			$results[] = new $className(0);
			
			# Assign the correct ID to this object
			$results[$resultIndex]->id = $data['id'];

			//$results[$resultIndex]->populate();
			# Assign this object the data related to it, and remember to mark it as having already retrieved its
			# data with the $hasData property, else it will try and populate itself.
			ActiveRecord::$cache[$className]['data'][$data['id']] = $data;
			# Data is cached in the same manner as is documented in the populate method.
			$results[$resultIndex]->data =& ActiveRecord::$cache[$className]['data'][$data['id']];
			$results[$resultIndex]->hasData = true;

			# We must call the __load hook for relevant data transformations.
			if ($callLoadMagic)
			{
				# The data objects may be cached from an existing item - if so there is no need to call them
				# all over again.
				if (!array_key_exists('transformed', ActiveRecord::$cache[$className]) || !array_key_exists($data['id'], ActiveRecord::$cache[$className]['transformed']))
				{
					# Not cached. Populate the transformed data.
					$results[$resultIndex]->__load();
				}
				else
				{
					# It is cached so we link it to the cache.
					$results[$resultIndex]->transformed =& ActiveRecord::$cache[$className]['transformed'][$data['id']];
				}
			}

			# Increment the array index for the next object.
			$resultIndex++;
		}

		# And we return the completed array.
		return $results;
	}

}